package com.thghh.resource;

import com.thghh.resource.util.NullArgumentException;

import java.io.IOException;
import java.io.Reader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * A {@link ResourceLoader} that uses a set of other loaders to load the Resources. On every request, loaders are
 * queried in the order of their appearance in the array of loaders provided to the constructor. However, by default, if
 * a request for some Resource name was already satisfied in the past by one of the loaders, that loader is queried
 * first (stickiness). This behavior can be disabled with {@link #setSticky(boolean)}, then the loaders are always
 * queried in the order of their appearance in the array.
 * 
 * <p>This class is thread-safe.
 */
public class MultiResourceLoader implements StatefulResourceLoader {

    private final ResourceLoader[] ResourceLoaders;
    private final Map<String, ResourceLoader> lastResourceLoaderForName
            = new ConcurrentHashMap<>();
    
    private boolean sticky = true;

    /**
     * Creates a new instance that will use the specified Resource loaders.
     * 
     * @param ResourceLoaders
     *            the Resource loaders that are used to load Resources, in the order as they will be searched
     *            (except where {@linkplain #setSticky(boolean) stickiness} says otherwise).
     */
    public MultiResourceLoader(ResourceLoader[] ResourceLoaders) {
        NullArgumentException.check("ResourceLoaders", ResourceLoaders);
        this.ResourceLoaders = ResourceLoaders.clone();
    }

    @Override
    public Object findResource(String name)
            throws IOException {
        ResourceLoader lastResourceLoader = null;
        if (sticky) {
            // Use soft affinity - give the loader that last found this
            // resource a chance to find it again first.
            lastResourceLoader = lastResourceLoaderForName.get(name);
            if (lastResourceLoader != null) {
                Object source = lastResourceLoader.findResource(name);
                if (source != null) {
                    return new MultiSource(source, lastResourceLoader);
                }
            }
        }

        // If there is no affine loader, or it could not find the resource
        // again, try all loaders in order of appearance. If any manages
        // to find the resource, then associate it as the new affine loader
        // for this resource.
        for (ResourceLoader ResourceLoader : ResourceLoaders) {
            if (lastResourceLoader != ResourceLoader) {
                Object source = ResourceLoader.findResource(name);
                if (source != null) {
                    if (sticky) {
                        lastResourceLoaderForName.put(name, ResourceLoader);
                    }
                    return new MultiSource(source, ResourceLoader);
                }
            }
        }

        if (sticky) {
            lastResourceLoaderForName.remove(name);
        }
        // Resource not found
        return null;
    }

    @Override
    public long getLastModified(Object ResourceSource) {
        return ((MultiSource) ResourceSource).getLastModified();
    }

    @Override
    public Reader getReader(Object ResourceSource, String encoding)
            throws IOException {
        return ((MultiSource) ResourceSource).getReader(encoding);
    }

    @Override
    public void closeResource(Object ResourceSource)
            throws IOException {
        ((MultiSource) ResourceSource).close();
    }

    /**
     * Clears the sickiness memory, also resets the state of all enclosed {@link StatefulResourceLoader}-s.
     */
    @Override
    public void resetState() {
        lastResourceLoaderForName.clear();
        for (ResourceLoader loader : ResourceLoaders) {
            if (loader instanceof StatefulResourceLoader) {
                ((StatefulResourceLoader) loader).resetState();
            }
        }
    }

    /**
     * Represents a Resource source bound to a specific Resource loader. It serves as the complete Resource source
     * descriptor used by the MultiResourceLoader class.
     */
    static final class MultiSource {

        private final Object source;
        private final ResourceLoader loader;

        MultiSource(Object source, ResourceLoader loader) {
            this.source = source;
            this.loader = loader;
        }

        long getLastModified() {
            return loader.getLastModified(source);
        }

        Reader getReader(String encoding)
                throws IOException {
            return loader.getReader(source, encoding);
        }

        void close()
                throws IOException {
            loader.closeResource(source);
        }

        Object getWrappedSource() {
            return source;
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof MultiSource) {
                MultiSource m = (MultiSource) o;
                return m.loader.equals(loader) && m.source.equals(source);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return loader.hashCode() + 31 * source.hashCode();
        }

        @Override
        public String toString() {
            return source.toString();
        }
    }

    /**
     * Show class name and some details that are useful in Resource-not-found errors.
     * 
     * @since 2.3.21
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("MultiResourceLoader(");
        for (int i = 0; i < ResourceLoaders.length; i++) {
            if (i != 0) {
                sb.append(", ");
            }
            sb.append("loader").append(i + 1).append(" = ").append(ResourceLoaders[i]);
        }
        sb.append(")");
        return sb.toString();
    }

    /**
     * Returns the number of {@link ResourceLoader}-s directly inside this {@link ResourceLoader}.
     * 
     * @since 2.3.23
     */
    public int getResourceLoaderCount() {
        return ResourceLoaders.length;
    }

    /**
     * Returns the {@link ResourceLoader} at the given index.
     * 
     * @param index
     *            Must be below {@link #getResourceLoaderCount()}.
     */
    public ResourceLoader getResourceLoader(int index) {
        return ResourceLoaders[index];
    }

    /**
     * Getter pair of {@link #setSticky(boolean)}.
     * 
     * @since 2.3.24
     */
    public boolean isSticky() {
        return sticky;
    }

    /**
     * Sets if for a name that was already loaded earlier the same {@link ResourceLoader} will be tried first, or
     * we always try the {@link ResourceLoader}-s strictly in the order as it was specified in the constructor.
     * The default is {@code true}.
     * 
     * @since 2.3.24
     */
    public void setSticky(boolean sticky) {
        this.sticky = sticky;
    }


}
